package de.chimos.property.util;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.ReadOnlyProperty;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;

public class PropertyWrapper<T>
{
	private final SimpleObjectProperty<T> value = new SimpleObjectProperty<T>();
	
	public final ObjectProperty<T> valueProperty()
	{
		updateValue();
		
		return value;
	}
	
	public final T getValue()
	{
		updateValue();
		
		return value.get();
	}
	
	public final void setValue(T value)
	{
		if(isValueWriteable() == false)
		{
			throw new IllegalArgumentException();
		}
		
		this.value.set(value);
	}
	
	private final ReadOnlyStringWrapper name = new ReadOnlyStringWrapper();
	public final ReadOnlyStringProperty nameProperty() { return name.getReadOnlyProperty(); }
	public final String getName() { return name.get(); }

	private final ReadOnlyStringWrapper humanReadableName = new ReadOnlyStringWrapper();
	public final ReadOnlyStringProperty humanReadableNameProperty() { return humanReadableName.getReadOnlyProperty(); }
	public final String getHumanReadableName() { return humanReadableName.get(); }
	
	private final ReadOnlyObjectWrapper<Object> bean = new ReadOnlyObjectWrapper<Object>();
	public final ReadOnlyObjectProperty<Object> beanProperty() { return bean.getReadOnlyProperty(); }
	public final Object getBean() { return bean.get(); };
	
	public static enum Type { Field, Classic, JavaFX }
	private final ReadOnlyObjectWrapper<Type> type = new ReadOnlyObjectWrapper<Type>();
	public final ReadOnlyObjectProperty<Type> typeProperty() { return type.getReadOnlyProperty(); }
	public final Type getType() { return type.get(); }

	private final ReadOnlyObjectWrapper<Class<?>> dataType = new ReadOnlyObjectWrapper<Class<?>>();
	public final ReadOnlyObjectProperty<Class<?>> dataTypeProperty() { return dataType.getReadOnlyProperty(); }
	public final Class<?> getDataType() { return dataType.get(); }
	
	private final ReadOnlyProperty<T> wrappedProperty;
	
	private final Method getterMethod;
	private final Method setterMethod;
	
	private final Field field;
	
	private boolean activeInternalUpdate = false;
	
	public PropertyWrapper(ReadOnlyProperty<T> property, String humanReadableName)
	{
		this.name.set(property.getName());
		this.humanReadableName.set(humanReadableName);
		this.bean.set(property.getBean());
		
		this.wrappedProperty = property;
		
		this.getterMethod = null;
		this.setterMethod = null;
		
		this.field = null;
		
		this.type.set(Type.JavaFX);

		if(property instanceof Property<?>)
		{
			this.value.bindBidirectional((Property<T>)this.wrappedProperty);
		}
		else
		{
			this.value.bind(this.wrappedProperty);
		}

		if(this.value.get() != null) {
			this.dataType.set(this.value.get().getClass());
		} else {
			this.dataType.set(null);
		}
		
		final PropertyWrapper<T> that = this;
		
		this.value.addListener(new ChangeListener<T>()
			{
				@Override
				public void changed(ObservableValue<? extends T> observable, T oldValue, T newValue)
				{
					if(that.value.get() != null) {
						that.dataType.set(that.value.get().getClass());
					} else {
						that.dataType.set(null);
					}
				}
			}
		);
	}
	
	public PropertyWrapper(final Object bean, final String name, String humanReadableName, final Method getterMethod, final Method setterMethod)
	{
		this.name.set(name);
		this.humanReadableName.set(humanReadableName);
		this.bean.set(bean);
		
		this.wrappedProperty = null;
		
		this.getterMethod = getterMethod;
		this.setterMethod = setterMethod;
		
		this.field = null;
		
		this.type.set(Type.Classic);
		this.dataType.set(null);
		
		final PropertyWrapper<T> that = this;
		
		this.value.addListener(new ChangeListener<T>()
			{
				@Override
				public void changed(ObservableValue<? extends T> observable, T oldValue, T newValue)
				{
					if(that.value.get() != null) {
						that.dataType.set(that.value.get().getClass());
					} else {
						that.dataType.set(null);
					}

					if(that.activeInternalUpdate == true)
					{
						return;
					}
					
					try {
						that.setterMethod.invoke(that.bean.get(), that.value.get());
					} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
						throw new IllegalStateException();
					}
				}
			}
		);
	}
	
	public PropertyWrapper(final Object bean, final String name, String humanReadableName, final Field field)
	{
		this.name.set(name);
		this.humanReadableName.set(humanReadableName);
		this.bean.set(bean);
		
		this.wrappedProperty = null;
		
		this.getterMethod = null;
		this.setterMethod = null;
		
		this.field = field;
		
		this.type.set(Type.Field);
		this.dataType.set(null);
		
		final PropertyWrapper<T> that = this;
		
		this.value.addListener(new ChangeListener<T>()
			{
				@Override
				public void changed(ObservableValue<? extends T> observable, T oldValue, T newValue)
				{
					if(that.value.get() != null) {
						that.dataType.set(that.value.get().getClass());
					} else {
						that.dataType.set(null);
					}

					if(that.activeInternalUpdate == true)
					{
						return;
					}
					
					try {
						that.field.set(that.bean.get(), that.value.get());
					} catch (IllegalArgumentException | IllegalAccessException e) {
						throw new IllegalStateException();
					}
				}
			}
		);
	}
	
	@SuppressWarnings("unchecked")
	public void updateValue()
	{
		switch(this.type.get())
		{
		case JavaFX:
			// nothing to do, thanks to binding
			return;
			
		case Classic:
			try
			{
				activeInternalUpdate = true;
				this.value.set((T)this.getterMethod.invoke(this.bean));
			}
			catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e)
			{
				activeInternalUpdate = false;
				throw new IllegalStateException(e);
			}
			activeInternalUpdate = false;
			return;
			
		case Field:
			try
			{
				activeInternalUpdate = true;
				this.value.set((T)this.field.get(this.bean));
			}
			catch (IllegalAccessException | IllegalArgumentException e)
			{
				activeInternalUpdate = false;
				throw new IllegalStateException(e);
			}
			activeInternalUpdate = false;
			return;
		}
		
		throw new IllegalStateException();
	}
	
	public boolean isValueWriteable()
	{
		switch(this.type.get())
		{
		case JavaFX:
			return (this.wrappedProperty instanceof Property<?>);
			
		case Classic:
			return (this.getterMethod != null);
			
		case Field:
			return true;
		}
		
		throw new IllegalStateException();
	}
}
